package com.crunch.core;

import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Engine {
	public Window window;
	public Input input;

	private ScheduledExecutorService timer;
	private boolean terminateFlag = false;

	// holds info for creating a new game state
	private static class NewGameState {
		public Class<? extends GameState> gameStateClass;
		public boolean active;
		public Object[] args;
		NewGameState(Class<? extends GameState> gameStateClass, boolean active, Object... args) {
			this.gameStateClass = gameStateClass;
			this.active = active;
			this.args = args;
		}
	}

	private ArrayList<GameState> gameStates = new ArrayList<GameState>();
	private ArrayList<NewGameState> newGameStates = new ArrayList<NewGameState>();

	private Comparator<GameState> gameStateOrderer = new Comparator<GameState>() {
		@Override
		public int compare(GameState o1, GameState o2) {
			if (o1.getUpdatePriority() < o2.getUpdatePriority()) {
				return -1;
			} else if (o1.getUpdatePriority() > o2.getUpdatePriority()) {
				return 1;
			} else {
				return 0;
			}
		}
	};

	public static interface Initializer {
		public void initialize(Engine engine);
	}
	private static Initializer initializer;

	public Engine(Initializer initializer) {
		Engine.initializer = initializer;

		window = new Window();
		window.setVisible(true);

		input = new Input(window);

		final int FPS = 60;
		timer = Executors.newSingleThreadScheduledExecutor();

		// first initialize the game states
		// notice that we use a delay of 1 so that the function is run on the same thread as future updates
		timer.schedule(new Runnable() {
			@Override
			public void run() {
				initializeGameStates();
			}
		}, 1, TimeUnit.MICROSECONDS);
		// again, delay of 1 so that init and update are run in FIFO order
		timer.scheduleAtFixedRate(new Runnable() {
			@Override
			public void run() {
				try {
					input.processEvents();
					update();
					// display runs on the OpenGL thread, but this thread waits for it to finish
					window.canvas.display();
					if (terminateFlag) {
						terminate();
					}
				} catch (Exception e) {
					e.printStackTrace();
					System.exit(1);
				}
			}
		}, 1, (long) (1000000.0 / (double) FPS), TimeUnit.MICROSECONDS);
	}

	public void shutdown() {
		terminateFlag = true;
	}

	private void terminate() {
		// destroy game states
		Iterator<GameState> it = gameStates.iterator();
		while (it.hasNext()) {
			GameState gs = it.next();
			gs.terminate(); // destroy the actors first
			gs.onDestroy();
			it.remove();
		}

		// this is called from the timer's own thread
		timer.shutdown();
		window.dispose();
	}

	private void initializeGameStates() {
		if (initializer == null) {
			System.out.println("Warning: no initializer provided");
		} else {
			initializer.initialize(this);
		}
	}

	private void update() {
		// activate/deactivate game states
		for (GameState gs : gameStates) {
			boolean active = gs.getActive();
			boolean shouldBeActive = gs.getShouldBeActive();
			if (active && !shouldBeActive) {
				gs.setActiveFlag(false);
				gs.onDeactivate();
			} else if (!active && shouldBeActive) {
				gs.setActiveFlag(true);
				gs.onActivate();
			}
		}

		// destroy game states
		Iterator<GameState> it = gameStates.iterator();
		while (it.hasNext()) {
			GameState gs = it.next();
			if (gs.getShouldDestroy()) {
				gs.terminate(); // destroy the actors first
				gs.onDestroy();
				it.remove();
			}
		}

		// create new game states
		for (NewGameState ngs : newGameStates) {
			GameState gs;
			try {
				gs = ngs.gameStateClass.newInstance();
			} catch (InstantiationException e) {
				System.out.println("Error: failed to instantiate class " + ngs.gameStateClass.getSimpleName());
				continue;
			} catch (IllegalAccessException e) {
				System.out.println("Error: failed to instantiate class " + ngs.gameStateClass.getSimpleName());
				continue;
			}
			gs.initialize(this, ngs.active);
			gameStates.add(gs);
			gs.onCreate(ngs.args);
		}
		newGameStates.clear();

		// ensure game states are ordered properly for the update phase (and for the beginning of the next update)
		Collections.sort(gameStates, gameStateOrderer);

		// call onUpdate for each game state
		for (GameState gs : gameStates) {
			gs.onUpdate();
			gs.update();
		}
	}

	// queues a game state to be created at the beginning of the next update
	public void createGameState(Class<? extends GameState> gameStateClass, boolean active, Object... args) {
		newGameStates.add(new NewGameState(gameStateClass, active, args));
	}

	public void destroyGameState(GameState gameState) {
		gameState.destroy();
	}
}
